0%

Size Balanced Tree

$\sf\large\text{Size Balanced Tree}$(节点大小平衡树,下简称SBT)是陈启峰在WC2007提出的一种新型平衡树。

SBT的高度为log n,其核心操作maintain()复杂度O(1),其他操作均为O(log n),所以是平衡树中非常优秀的一种。其主要通过子树大小来维持其平衡性质。

和其他平衡树一样,SBT支持大部分较为常规的操作:

  • “以x为根的子树”在下文中简称为”x子树”
1
2
3
4
5
6
7
insert(x,val):向x子树插入值为val的结点
del(x,val):删除x子树中值为val的结点
find(x,val):查找x子树中值为val的结点
rank(x,val):返回x子树中val的排名
kth(x,k):返回x子树中排名第k的结点(大小排序均可)
pre(x,val):返回x子树中val的前驱
suc(x,val):返回x子树中val的后继

$\sf\large\text{1.SBT的结点定义}$

1
2
3
4
5
6
7
8
9
10
#define ls(x) t[x].l
#define rs(x) t[x].r
//下文中的ls,rs均为此处宏定义
struct SBT
{
int l;//左子树
int r;//右子树
int val;//值
int siz;//子树大小
}t[maxn];

SBT有一个特殊的性质需要维护:某子树的大小大于等于其兄弟子树的大小。

直观写出来就是:

1
2
t[ls(i)].siz>=max(t[rs(rs(x))].siz,t[ls(rs(x))].siz);
t[rs(i)].siz>=max(t[ls(ls(x))].siz,t[rs(ls(x))].siz);

$\sf\large\text{2.SBT的左右旋}$

SBT也是需要旋转的,且同样分为左右旋两种。

我们列出旋转前后的状态,即

1
2
3
4
5
6
7
8
9
10
11
12
13
左旋前:
根:rt 左子树的根:L
右子树的根:R_rt 右子树的子树:R_l,R_r
左旋后:
根:R_rt 右子树的根:R_r
左子树的根:rt 左子树的子树:L,R_l

右旋前:
根:rt 右子树的根:R
左子树的根:L_rt 右子树的子树:L_l,L_r
右旋后:
根:L_rt 左子树的根:L_l
右子树的根:rt 右子树的子树:L_r,R

左右旋代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void lrot(int &x)
{
int y=rs(x);
rs(x)=ls(y);
ls(y)=x;
t[y].siz=t[x].siz;
t[x].siz=t[ls(x)].siz+t[rs(x)].siz+1;
x=y;
}

void rrot(int &x)
{
int y=ls(x);
ls(x)=rs(y);
rs(y)=x;
t[y].siz=t[x].siz;
t[x].siz=t[ls(x)].siz+t[rs(x)].siz+1;
x=y;
}

$\sf\large\text{3.SBT的维护}$

首先,假设一株满足条件的SBT长这样:

此时有A.siz,B.siz≤R.siz并且C.siz,D.siz≤L.siz

每当插入一个值的时候,平衡树的平衡性就可能被打破,所以我们要使用O(1)的maintain(x)对x子树进行修复操作。

插入后,可能会出现以下的四种情况:

1
2
3
4
* t[ls(ls(x))].siz>t[rs(x)].siz
* t[ls(rs(x))].siz>t[rs(x)].siz
* t[rs(rs(x))].siz>t[ls(x)].siz
* t[rs(ls(x))].siz>t[ls(x)].siz

直接分类处理会很复杂。但SBT的对称性质使我们只用讨论两种情况:

$\sf 1.t[ls(ls(x))].siz>t[rs(x)].siz$

看到上图,对于T子树,此时的情况就是A.siz>R.siz,显然此时就导致平衡性质受损。

第一步,我们先将其右旋,得到下面这株可能仍然不满足性质的树:

之所以说可能仍不满足性质,是因为C.siz>B.siz或D.siz>B.siz的情况可能发生,所以有必要再次使用maintain(T)。这样一来,L的右子树就会被多次调整,不过别担心,每次调整都是O(1)的。

$\sf 1.t[rs(ls(x))].siz>t[rs(x)].siz$

(为了直观,此处增加结点数)上述情况在下图中表示为:B.siz>R.siz

那么对称地,我们先进行左旋,得到:

不同的是,接下来我们还要进行一次右旋,得到:

有同学可能就会问了:左旋右旋过后这棵树就很不稳定了,怎么办?

答:没事,至少图中的小子树还是满足性质的,所以只要maintain一下L和T就可以让L,T子树平衡。最后再来一次maintain(B)即可。

情况3,4与上述两种情况分别相反,对应操作即可,此处不再赘述。

maintain采用递归实现,具有一定的对称美感。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
void maintain(int &x,bool lr)
{
if(lr)//左边
{
if(t[ls(ls(x))].siz>t[rs(x)].siz)//#1
{
rrot(x);
}
else if(t[rs(ls(x))].siz>t[rs(x)].siz)//#2
{
lrot(ls(x));
rrot(x);
}
else
{
return ;
}
}
else//右边
{
if(t[rs(rs(x))].siz>t[ls(x)].siz)//#3
{
lrot(x);
}
else if(t[ls(rs(x))].siz>t[ls(x)].siz)//#4
{
rrot(rs(x));
lrot(x);
}
else
{
return ;
}
}
maintain(ls(x),1);
maintain(rs(x),0);
maintain(x,0);
maintain(x,1);
}

$\sf\large\text{4.插入元素}$

SBT的插入和其他平衡树基本一致,只是向非空子树插入元素时要maintain一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void insert(int &x,int val)
{
if(x==0)
{
x=++cnt;
ls(x)=rs(x)=0;
t[x].siz=1;
t[x].val=val;
}
else
{
t[x].siz++;
if(val<t[x].val)
{
insert(ls(x),val);
}
else
{
insert(rs(x),val);
}
maintain(x,val<t[x].val);
}
}

$\sf\large\text{5.查找前驱&后继}$

查找前驱函数pre(),我们传入三个参数:

  • x:当前子树
  • p:保存的前驱结点
  • val:查找值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int pre(int &x,int p,int val)
{
if(!x)
{
return p;
}
if(t[x].val>=val)
{
return pre(ls(x),p,val);
}
else
{
return pre(rs(x),x,val);
}
}

后继同理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int suc(int &x,int p,int val)
{
if(!x)
{
return p;
}
if(t[x].val>val)
{
return suc(ls(x),x,val);
}
else
{
return suc(rs(x),p,val);
}
}

$\sf\large\text{6.删除元素}$

删除元素的操作与BST的删除基本一致。删除之后maintain也是没有必要的,其原因是:

虽然不能保证删完后还是SBT,但是树的最大深度不会变化,时间复杂度也并不变化,maintain就显得多余了。

删除有两种主流方法,均可使用:

1.后继替换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
int del(int &x,int val)
{
t[x].siz--;
if(val>t[x].val)
{
del(rs(x),val);
return ;
}
else if(val<t[x].val)
{
del(ls(x),val);
return ;
}

if(ls(x)&&!rs(x))
{
int reg=x;
x=ls(x);
return reg;
}
else if(!ls(x)&&rs(x))
{
int reg=x;
x=rs(x);
return reg;
}
else if(!ls(x)&&!rs(x))
{
int reg=x;
x=0;
return reg;
}
else
{
int reg=rs(x);
while(ls(res))
{
reg=ls(reg);
}
t[x].val=t[reg].val;
del(rs(x),t[reg].val);
}
}//reg是临时暂存变量

2.前驱替换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

int del(int &x,int val)
{
int reg;
t[x].size--;
if((val==t[x].val)||(val<t[x].val&&!ls(x))||(val>t[x].val&&!rs(x)))
{
reg=t[x].val;
if(ls(x)&&rs(x))
{
t[x].val=del(ls(x),t[x].val+1);
}
else
{
x=ls(x)+rs(x);
}
}
else if(val>t[x].val)
{
reg=del(rs(x),val);
}
else if(val<t[x].val)
{
reg=del(ls(x),val);
}
return reg;
}

$\sf\large\text{7.一系列的查询操作}$

1.最值查询(单向爬树)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int extremum(int x,bool minmax)
{
if(minmax)//最小值
{
while(ls(x))
{
x=ls(x);
}//一直向左爬
}
else//最大值
{
while(rs(x))
{
x=rs(x);
}//一直向右爬
}
return t[x].val;
}

2.查询第k小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int kth(int &x,int k)
{
int cur=t[ls(x)].siz+1;
if(cur==k)
{
return t[x].val;
}
else if(cur>k)
{
return kth(ls(x),k);
}
else
{
return kth(rs(x),k-cur);
}
}

若要查询第k大,则对几个选择结构中的内容进行交换并调整即可。

3.查找排名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int rank(int &x,int val)
{
if(val==t[x].val)
{
return t[ls(x)].siz+1;
}
else if(val<t[x].val)
{
return rank(ls(x),val);
}
else
{
rank(rs(x),val)+t[ls(x)].siz+1;
}
}

关于树高log n的证明以及maintain时间复杂度O(1)的证明,陈启峰在论文中有提到,详见此处

最后,以LuoGu3369作为例题,SBT的代码实现见此,用时142ms,已经算是很优秀的平衡树了。